# -*- coding: utf-8 -*-
# Purpose: HydroPlatform tools
# Created: 11/26/2008
# Copyright (c) 2008,2009 by University College London
# Authors:
# Didrik Pinte <dpinte@dipole-consulting.com>
#
# This program is free software under the GPL (>=v2)
# Read the file COPYING coming with HydroPlatform for details.
'''
The tools module contains all the tools that will interact with the graphical 
view of the network (thus mainly the hydroplatform.model.layer.NetworkLayer 
objects).

All the tools are children of Thuban.UI.viewport.Tool classes
'''
import logging
import wx

from sqlalchemy import or_, and_
from sqlalchemy.exc import IntegrityError


from Thuban.UI.viewport import Tool

from hydroplatform.model.layer import NetworkLayer
from hydroplatform.model import database as db
from hydroplatform.model import network as nt
from hydroplatform.model import networkfun
from hydroplatform.ui.objecteditordialog import ObjectEditorDialog

class ConvertCoordMixin(object):
    '''
    Mixin adding functions to convert coordinates from the screen to the map
    and to the layer
    '''
    
    def getlayer_coord(self, cx1, cy1, layer):
        '''
        Convert pixel coordinaters to layer coordinates
        '''
        if self.view.map.GetProjection() is not None:
            cx1, cy1 = self.view.map.GetProjection().Inverse(cx1, cy1)
        if layer.projection is not None:
            cx1, cy1 = layer.projection.Forward(cx1, cy1)
        return cx1, cy1
    
    def getmap_coord(self, cx1, cy1, layer):
        '''
        Convert layer coordinaters to map coordinates
        '''
        if layer.projection is not None:
            cx1, cy1 = layer.projection.Inverse(cx1, cy1)
        if self.view.map.GetProjection() is not None:
            cx1, cy1 = self.view.map.GetProjection().Forward(cx1, cy1)
        return cx1, cy1
        
    def getlayer_bbox(self, bbox, layer):
        '''
        Convert a map bbox to a layer bbox
        '''
        if self.view.map.GetProjection() is not None:
            bbox = self.view.map.GetProjection().InverseBBox(bbox)
        if layer.projection is not None:
            bbox = layer.projection.ForwardBBox(bbox)
        return bbox

class SelectObjectTool(Tool, ConvertCoordMixin):
    '''
    Base tool for all the selection tools
    '''

    def __init__(self, view, context):
        Tool.__init__(self, view)
        self.context = context
        self.dlg = None
        self.selected_object = None
        self.selected_layer = None
    
    def Name(self):
        '''
        Returns the tool name
        
        Overloaded method from Tools
        '''
        return "SelectObjectTool"

    def MouseUp(self, event):        
        '''
        On mouseup, selects the closest object in the layer
        
        Overloaded method from Tools
        '''
        maxsize = 5 # uses a bounding box of 5 pixels around the click
        box = self.view.unprojected_rect_around_point(event.m_x, 
                                                      event.m_y, 
                                                      maxsize)
        for layer in [layer for layer in self.view.map.Layers() \
                      if isinstance(layer, NetworkLayer)]:
            bbox = self.getlayer_bbox(box, layer)
            obj = layer.find_closest_node_in_box(bbox, unselect=True)
            if obj is not None:
                self.selected_layer = layer
                self.selected(obj)
                break
        else:
            self.selected_object = None
            self.selected_layer = None
            self.context.mainwindow.set_position_text(
                "No node under you click")
        self.view.full_redraw()
        
        
    def MouseDoubleClick(self, event):
        '''
        On double click show the node editor
        
        Overloaded method from Tools
        '''
        if self.selected_object is not None:
            frame = ObjectEditorDialog(self.context.mainwindow, 
                                       self.context, 
                                       self.selected_object)
            frame.ShowModal() 
            
    def selected(self, network_object):
        '''
        Does something with the selected network_object
        '''
        self.selected_object = network_object

class IdentifyObjectTool(SelectObjectTool):
    '''
    Identifies object in layers
    '''

    def __init__(self, view, context):
        SelectObjectTool.__init__(self, view, context)
    
    def Name(self):
        '''
        Returns the tool name
        
        Overloaded method from Tools 
        '''
        return "IdentifyObjectTool"

    def selected(self, network_object):
        '''
        Show the name of the select object in mainwindow
        '''
        self.selected_object = network_object
        # set the object selected attribute so that it is rendered has selected
        network_object.selected = True
        self.context.mainwindow.set_position_text(u"Object name : %s" % \
                                                  network_object.getName())
 
class DeleteObjectTool(SelectObjectTool):
    '''
    Delete object from layer, network and db tool
    '''
    
    def __init__(self, view, context):
        SelectObjectTool.__init__(self, view, context)
    
    def Name(self):
        '''
        Returns the tool name
        
        Overloaded method from Tools 
        '''
        return "DeleteObjectTool"

    def selected(self, network_object):
        '''
        Delete the selected network_object
        '''
        delete_network_object(self.context, self.selected_layer.network, 
                              network_object)

 
class MoveNodeTool(Tool, ConvertCoordMixin):
    '''
    Mode node tool in the layer
    '''
    def __init__(self, view, context):
        Tool.__init__(self, view)
        self.context = context
        self.selected_layer = None
    
    def Name(self):
        '''
        Returns the tool name
        
        Overloaded method from Tools 
        '''
        return "MoveNodeTool"

    def select_closestnode(self, event):
        '''
        Select the closest node in NetworkLayer
        '''
        maxsize = 20
        box = self.view.unprojected_rect_around_point(event.m_x, 
                                                      event.m_y, 
                                                      maxsize)
        node = None
        for layer in [layer for layer in self.view.map.Layers() \
                      if isinstance(layer, NetworkLayer)]:
            bbox = self.getlayer_bbox(box, layer)
            node = layer.find_closest_node_in_box(bbox, unselect=True)
            self.selected_layer = layer
            if node is not None:
                return node
        return node
    
    def MouseDown(self, event):
        '''
        On mouse down, select the closes node
        
        Overloaded method from Tools 
        '''
        # select closest
        Tool.MouseDown(self, event)
        self.node = self.select_closestnode(event)
        if self.node is not None:
            self.node.selected = True
     
    def draw(self, devc):
        '''
        Draw a red circle under the mouse
        
        Overloaded method from Tools 
        '''
        # draw a red circle at position 
        devc.SetPen(wx.RED_PEN)
        devc.SetBrush(wx.RED_BRUSH)
        devc.SetLogicalFunction(wx.XOR)
        devc.DrawCircle(self.current[0], self.current[1], 5)
        
    def MouseUp(self, event):
        '''
        On mouse up, save the new coordinates
        
        TODO : should we ask user confirmation ?
        
        Overloaded method from Tools 
        '''
        Tool.MouseUp(self, event)
        # save node position
        (cox, coy) = self.view.win_to_proj(event.m_x, event.m_y)
        cox, coy = self.getlayer_coord(cox, coy, self.selected_layer)
        if self.node is not None and hasattr(self.node, "set_coord"):
            self.node.set_coord(cox, coy, self.context.session.dbsession)
            self.context.session.dbsession.commit()
            self.node.selected = False
        self.view.full_redraw()

        
class AddNodeTool(Tool, ConvertCoordMixin):
    '''
    Add node to the session NetworkLayer
    '''
    
    def __init__(self, view, context, nodetype):
        Tool.__init__(self, view)
        self.nodetype = nodetype
        # tool name is nodetype specific !
        self.tool_name = "AddNodeTool" + str(self.nodetype.id) 
        self.context = context

    def Name(self):
        '''
        Tool name is specific to the nodetype (see __init__)
        
        Overloaded method from Tools 
        '''
        return self.tool_name
   
    def MouseUp(self, event):
        '''
        On mouse up, create the new node
        
        Overloaded method from Tools 
        '''
        # get coordinates
        (cox, coy) = self.view.win_to_proj(event.m_x, event.m_y)
        # get current project and network
        cnetwork = self.context.session.hpnetwork
        cproject = cnetwork.project
        # ask user the new node name
        dialog = wx.TextEntryDialog(None,
                    "Enter node name",
                    "New %s node in network %s" % (self.nodetype.name,
                                                   cnetwork.name))
        if dialog.ShowModal() == wx.ID_OK:
            dbsession =  self.context.session.dbsession
            name = dialog.GetValue()  
            if len(name) > 0:
                try:
                    # create new node
                    newnode = db.Node(name, cproject, self.nodetype)  
                    dbsession.add(newnode)
                    # get first layer the upper most layer from map 
                    # that is a hydroplatform layer
                    network_layer = None
                    for layer in self.view.map.Layers():
                        if isinstance(layer, NetworkLayer):
                            network_layer = layer
                            break
                    if network_layer is None:
                        raise Exception("No network layer loaded")
                    cox, coy = self.getlayer_coord(cox, coy, network_layer)
                    newnode.set_coord(cox, coy)
                    dbsession.commit()
                    # add it to the current network
                    cnetwork.add_node(newnode)
                    dbsession.commit()
                    # refresh the network object in the network_layer !
                    dbsession.refresh(network_layer.network)
                    dbsession.refresh(cnetwork)
                except IntegrityError, exception:
                    # name is not unique
                    logging.error(exception)
                    self.context.mainwindow.RunMessageBox(
                        'Duplicate node name error',
                        '''Node name must be unique. A node with the same 
                        name already exists for this project''',
                             wx.OK | wx.ICON_EXCLAMATION) 
                    del newnode
                    dbsession.rollback()
                except Exception, exception:
                    dbsession.rollback()                
                    # FIXME : do not raise again but manage properly
                    raise exception
        self.view.full_redraw() 

class AddLinkTool(Tool, ConvertCoordMixin):
    '''
    Add link to the NetworkLayer of the nodes that will be linked
    '''
    def __init__(self, view, context):
        Tool.__init__(self, view)
        self.context = context 
        self.selection_layer = None
        self.ccoord = None
        self.scoord = None
        self.end_node = None
        self.start_node = None
        
    def Name(self):
        '''
        Returns the tool name
        
        Overloaded method from Tools 
        '''
        return "AddLinkTool"

    def select_closestnode(self, pixel_coord, layer_lookup=True):
        '''
        Select the closest node to the pixe_coord provided 
        
        if layer_lookup is True and selection_layer is None,
        it parses the map layers for all the NetworkLayer. Otherwise, it looks
        only in the selection_layer.
        
        FIXME : should be merged with the ModeNodeTool
        '''
        # project coord !
        coord = self.view.win_to_proj(pixel_coord[0], pixel_coord[1])
        node = None
        if self.selection_layer is None:
            if layer_lookup is False:
                return None 
            else:
                for layer in [layer for layer in self.view.map.Layers() \
                              if isinstance(layer, NetworkLayer)]:
                    point = self.getlayer_coord(coord[0], coord[1], layer) 
                    node = layer.find_closest_node(point)
                    if node is not None:
                        self.selection_layer = layer
                        break
        else:
            point = self.getlayer_coord(coord[0],
                                        coord[1], 
                                        self.selection_layer) 
            node = self.selection_layer.find_closest_node(point)
        return node
        
    
    def MouseDown(self, event):
        '''
        Select closest node as starting node
        
        Overloaded method from Tools 
        '''
        # select closest
        Tool.MouseDown(self, event)
        self.start_node = self.select_closestnode(self.start)
        self.ccoord = None
        self.end_node = None
        self.current = None
        self.scoord = self.set_coord_fromnode(self.start_node)
        
     
    def draw(self, devc):
        '''
        Draw the link between the start node and the end node if any 
        during dragging
        
        Overloaded method from Tools 
        '''
        if self.scoord is not None and self.ccoord is not None:
            devc.DrawLine(self.scoord[0], self.scoord[1], \
                    self.ccoord[0], self.ccoord[1])
    
    def MouseMove(self, event):
        '''
        Try to find the closest node different than the starting 
        
        Overloaded method from Tools 
        '''
        Tool.MouseMove(self, event)
        if self.current is not None:
            # layer_lookup set to False to ensure that the end_node is in the 
            # same layer as the start_node
            self.end_node = self.select_closestnode(self.current, False)
        self.ccoord = self.set_coord_fromnode(self.end_node)

    def set_coord_fromnode(self, node):
        '''
        Set the self.ccoord (current coordinates) using the given node
        '''
        if node is not None:
            cox, coy = self.getmap_coord(node.x, 
                                          node.y, 
                                     self.selection_layer)
            return self.view.proj_to_win(cox, coy)        
        
    def MouseUp(self, event):
        '''
        Save the new link betweent the selected starting and ending nodes
        
        Overloaded method from Tools 
        '''
        Tool.MouseUp(self, event)
        if self.start_node is None or self.end_node is None:
            return
        # create new link if not existing
        network = self.selection_layer.network
        project = network.project
        dbsession = self.context.session.dbsession
        if network.has_link(self.start_node, self.end_node):
            # FIXME : what should we do with existing links : allow add more ? 
            logging.info('Link already exists')            
            return
        elif self.start_node == self.end_node:
            logging.info('Start node and end node must be different')
            return
        else:
            # allowed linktypes are the intersection between the authorized
            # link types for start node and for end node
            # slinkset = set(self.start_node.type.linktypes)
            # elinkset = set(self.end_node.type.linktypes)
            #linktypes = list(linkset.intersection(elinkset))
            # Behaviour changed - all the link types are allowed
            linktypes = project.linktypes
            if len(linktypes) ==0 :
                raise Exception(u'''No link types available for those nodes.\n
                                    You have to associate link types to node 
                                    types in the node type editor''')
            elif len(linktypes) == 1:
                linktype = linktypes[0]
            else:
                # show dialog to choose the link type
                dialog = wx.SingleChoiceDialog(self.context.mainwindow,
                                       u"Link type", 
                                       u"Choose a link type",
                                       choices = [l.name for l in linktypes])
                ret = dialog.ShowModal()
                if ret == wx.ID_OK:
                    linktypename = dialog.GetStringSelection()
                    for linkt in linktypes:
                        if linkt.name == linktypename:
                            linktype = linkt
                            break
                    else:
                        raise Exception(u'''Link type not in list ... 
                                        very strange''')
                else:
                    return
            try:
                link = db.Link(project, 
                               self.start_node, 
                               self.end_node, 
                               linktype)
                dbsession.add(link)
                dbsession.commit()
                network.add_link(link)                
                dbsession.commit()
                dbsession.refresh(network)
            except IntegrityError, exception:
                dbsession.rollback()
                dialog = wx.MessageDialog(self.context.mainwindow, 
                            u"Integrity error while adding the link :\n%s" % \
                               exception,
                            caption="Integrity error",
                            style = wx.ICON_ERROR |  wx.OK)
                dialog.ShowModal()
            self.view.full_redraw()
            
def delete_network_object(context, network, network_object):
    '''Delete the given network_object
    
    Parameters :
    ------------ 
    context : Thuban context 
        Context with a sqlalchemy session and a project
    network : Network
        The network on wich we will operate
    network_object : Link/Node
        the object to be deleted
    '''
    dbsession = context.session.dbsession
    try:
        otype = "node " if isinstance(network_object, nt.Node) else "link"
        
        return_code = \
                context.mainwindow.RunMessageBox("Delete object tool", \
                  '''Are you sure you want to delete this  %s ?''' % otype, \
                     wx.YES_NO | wx.ICON_QUESTION)
        if return_code == wx.ID_NO:
            return
        if isinstance(network_object, nt.Node):
            # check if node belongs to other networks
            nodes = dbsession.query(db.Node).filter( \
                and_ (db.Node.id == network_object.id,
                      db.Network.id == network.id)).all()
            if len(nodes) > 1:
                return_code = \
                    context.mainwindow.RunMessageBox("Delete object tool", \
                      '''This node is also part of other networks\n
                          Do you want to remove this node from the project ?''', \
                         wx.YES_NO | wx.ICON_QUESTION)
                if return_code == wx.ID_YES:
                    # delete the node from the project and all the networks
                    networkfun.delete_from_project(dbsession, network.project, 
                                             network_object)
                else:
                    # delete the node only from the current project
                    networkfun.delete_node_from_network(dbsession, network, 
                                                  network_object)
            else:
                # delete the node from the project as it belongs to only 
                # one network
                # TODO : this behaviour could be customized
                networkfun.delete_from_project(dbsession, network.project, 
                                         network_object)
                
        elif isinstance(network_object, nt.Link):
           # check if node belongs to other networks
            links = dbsession.query(db.Link).filter( \
                and_(db.Link.id == network_object.id,
                     db.Network.id == network.id)).all() 
            if len(links) >1:
                return_code = \
                    context.mainwindow.RunMessageBox("Delete object tool", \
                      '''This link is also part of other networks\n
                          Do you want to remove this link from the project ?''', \
                         wx.YES_NO | wx.ICON_QUESTION)
                if return_code == wx.ID_YES:
                    # delete the link from the project and all the networks
                    networkfun.delete_from_project(dbsession, network.project, 
                                             network_object)
                else:
                    # delete the node only from the current project
                    network.remove_link(network_object)
            else:
                # delete the node from the project as it belongs to only 
                # one network
                # TODO : this behaviour could be customized
                networkfun.delete_from_project(dbsession, network.project, 
                                         network_object)
                 
        dbsession.commit()
        dbsession.refresh(network)
        context.mainwindow.canvas.full_redraw()
    except Exception, exception:
        logging.exception(exception)
        dbsession.rollback()
        raise exception
    
